package main

import (
	"baikai/ptkGUI/config"
	"errors"
	"fmt"
	"net"
	"os"
	"path"
	"strconv"
	"strings"
	"time"

	"github.com/gdamore/tcell/v2"
	"github.com/rivo/tview"
	"gopkg.in/yaml.v2"
)

// 设置了 header 的话，这里需要改为1
const tableStartRow = 0

func main() {
	app, _ := NewApp("config.yaml")
	_ = app.Run()
}

type App struct {
	app *tview.Application

	banner    tview.Primitive
	appPanel  tview.Primitive
	pages     *tview.Pages
	treePanel *tview.TreeView
	oprPanel  *tview.Flex
	logPanel  *tview.TextView

	dbServersNode *tview.TreeNode
	cmServersNode *tview.TreeNode

	// config fields
	outFile   string
	global    *config.Global
	dbServers []*config.DBServer
	cmServers []*config.CMServer
	comment   string
}

func NewApp(filepath string) (*App, error) {
	app := &App{
		outFile: filepath,
		app:     tview.NewApplication(),
		pages:   tview.NewPages(),
	}
	if err := app.checkOutFile(); err != nil {
		return nil, err
	}
	app.setup()
	return app, nil
}

func (a *App) Run() error {
	tview.Styles = baseTheme
	return a.app.SetRoot(a.appPanel, true).
		SetFocus(a.treePanel).
		EnableMouse(true).
		EnablePaste(true).
		Run()
}

const (
	pageGlobal           = "page-global"
	pageSSHConfig        = "page-ssh-config"
	pageGlobalCMOption   = "page-global-cm-option"
	pageGlobalAdvanced   = "page-global-advanced"
	pageGlobalAddHBA     = "page-global-add-hba"
	pagePreview          = "page-preview"
	pageAddDBServer      = "page-add-db-server"
	pageAddCMServer      = "page-add-cm-server"
	pageDBServerAdvanced = "page-db-server-advanced"
	pageModal            = "page_modal"
	pageServer           = "page-%s-%s"
)

var (
	dbRolePrimary        = "primary"
	dbRoleStandby        = "standby"
	dbRoleCascadeStandby = "cascade_standby"
	dbRoles              = []string{
		dbRolePrimary,
		dbRoleStandby,
		dbRoleCascadeStandby,
	}

	shortRoleMap = map[string]string{
		dbRolePrimary:        "P",
		dbRoleStandby:        "S",
		dbRoleCascadeStandby: "C",
	}
)

func (a *App) setup() {
	a.initBanner()
	a.initGlobalPage()
	a.pages.ShowPage(pageGlobal)
	a.initTreePanel()
	a.initOprPanel()
	a.initLogPanel()
	a.initAppPanel()
}

func (a *App) toYAML() string {
	cfg := config.Config{
		Global:    a.global,
		DBServers: a.dbServers,
		CMServers: a.cmServers,
		Comment:   a.comment,
	}
	bs, err := yaml.Marshal(cfg)
	if err != nil {
		panic(err)
	}
	return string(bs)
}

// randomName returns a random cluster name
func randomName() string {
	return "c1"
}

func (a *App) parseDuration(s string, reciever *time.Duration) {
	a.ClearLog()
	if s == "" {
		return
	}
	// 只输入数字，默认单位s
	if v, err := strconv.Atoi(s); err == nil {
		a.Warn("不带单位时，默认采用单位 s (秒)")
		*reciever = time.Duration(v) * time.Second
		return
	}

	d, err := time.ParseDuration(s)
	if err != nil {
		a.Error(fmt.Sprintf("无法识别的超时时间: %s", s))
		return
	}
	*reciever = d
}

func (a *App) initGlobalPage() {
	g := &config.Global{
		ClusterName: randomName(),
		User:        "omm",
		Group:       "omm",
		BaseDir:     "/opt/mogdb",
		DBPort:      26000,
		EnvMap:      make(map[string]string),
	}
	form := NewXForm()
	form.AddInputField("集群名", g.ClusterName, "推荐使用英文字母和数字组合", 50, nil, func(text string) { g.ClusterName = text }).
		AddInputField("用户名", g.User, "默认值 omm", 50, nil, func(text string) { g.User = text }).
		AddInputField("用户组", g.Group, "默认值 omm", 50, nil, func(text string) { g.Group = text }).
		AddInputField("用户密码", g.UserPassword, "选填，注意某些 OS 默认必须设置才能使用 SSH 登录", 50, nil, func(text string) { g.UserPassword = text /* TODO enrypt */ }).
		AddInputField("数据库密码", g.DBPassword, "数字/大小写字母/特殊字符, 不少于三种组合，8~15位", 50, nil, func(text string) { g.DBPassword = text /* TODO enrypt */ }).
		AddInputField("数据库端口", strconv.Itoa(g.DBPort), "默认26000，取值范围 [1024, 65535]", 50, a.validatePort,
			func(s string) { g.DBPort, _ = strconv.Atoi(s) }).
		AddInputField("安装目录", g.BaseDir, "填写绝对路径，无需提前创建，PTK会自动为每个节点创建", 50, nil, func(text string) { g.BaseDir = text }).
		AddInputField("集群备注", a.comment, "选填", 50, nil, func(text string) { a.comment = text }).
		AddButton("高级配置", func() { a.showGlobalAdvancedConfigPage(pageGlobal) }).
		AddButton("配置通用SSH凭证", func() { a.showSSHConfigPage(pageGlobal) }).
		AddButton("配置CM相关参数", func() { a.showGlobalCMOptionConfigPage(pageGlobal) })

	form.SetBorder(true).SetTitle("Global配置").
		SetBorderPadding(1, 1, 2, 2)

	a.global = g
	a.pages.AddPage(pageGlobal, form, true, false)
	a.pages.AddPage(pageGlobalAdvanced, NewXForm(), true, false)
}

func (a *App) showSSHConfigPage(current string) {
	sshOpt := a.global.SSHOption
	if sshOpt == nil {
		sshOpt = &config.SSHOption{
			Port: 22,
			User: "root",
		}
	}
	connTimeout := ""
	if sshOpt.ConnTimeout > 0 {
		connTimeout = sshOpt.ConnTimeout.String()
	}
	execTimeout := ""
	if sshOpt.ExecTimeout > 0 {
		execTimeout = sshOpt.ExecTimeout.String()
	}
	form := NewXForm().
		AddInputField("端口", strconv.Itoa(sshOpt.Port), "默认值 22", 50, a.validatePort, func(text string) { sshOpt.Port, _ = strconv.Atoi(text) }).
		AddInputField("用户", sshOpt.User, "默认值 root", 50, nil, func(text string) { sshOpt.User = text }).
		AddInputField("密码", sshOpt.Password, "密码和密钥二选一即可", 50, nil, func(text string) { sshOpt.Password = text }).
		AddInputField("私钥路径", sshOpt.KeyFile, "密码和密钥二选一即可，默认 ${HOME}/.ssh/id_rsa", 50, nil, func(text string) { sshOpt.KeyFile = text }).
		AddInputField("私钥密码", sshOpt.Passphrase, "", 50, nil, func(text string) { sshOpt.Passphrase = text }).
		AddInputField("登录超时时间", connTimeout, "默认 1m, 支持单位 h (时)/m (分)/s (秒)", 50, nil, func(text string) {
			a.parseDuration(text, &sshOpt.ConnTimeout)
		}).
		AddInputField("执行超时时间", execTimeout, "默认 10m, 支持单位 h (时)/m (分)/s (秒)", 50, nil, func(text string) {
			a.parseDuration(text, &sshOpt.ExecTimeout)
		}).
		AddButton("保存", func() {
			a.ClearLog()
			if sshOpt.Password == "" && sshOpt.KeyFile == "" {
				a.Error("密码和密钥至少需要填写一个")
				return
			}
			if sshOpt.KeyFile != "" {
				if _, err := os.Stat(sshOpt.KeyFile); errors.Is(err, os.ErrNotExist) {
					a.Error(fmt.Sprintf("密钥文件 %s 不存在", sshOpt.KeyFile))
					return
				}
			}
			a.global.SSHOption = sshOpt

			a.pages.SwitchToPage(current)
			a.pages.RemovePage(pageSSHConfig)
		})
	page := a.makePageWithCloseButton(form, "配置SSH通用凭证", current, pageSSHConfig, nil)
	a.pages.AddPage(pageSSHConfig, page, true, false)
	a.pages.SwitchToPage(pageSSHConfig)
}

func (a *App) addKVTableRow(t *tview.Table, text string) bool {
	a.ClearLog()
	if text == "" {
		return true
	}
	pieces := strings.SplitN(text, "=", 2)
	if len(pieces) != 2 {
		a.Error(fmt.Sprintf("参数 %s 格式不正确，格式应为 <KEY>=<VALUE>", text))
		return false
	}
	key := strings.TrimSpace(pieces[0])
	value := strings.TrimSpace(pieces[1])

	return a.addTableRow(t, key, value)
}

// 注意：cols[0] 必须为唯一值，存在则更新，不存在则追加
func (a *App) addTableRow(t *tview.Table, cols ...string) bool {
	if len(cols) == 0 {
		return true
	}
	a.ClearLog()
	row := t.GetRowCount()
	for i := tableStartRow; i < row; i++ {
		if t.GetCell(i, 1).Text == cols[0] {
			// update
			for j := 1; j < len(cols); j++ {
				t.SetCell(i, j+1, NewTableCell(cols[j]).SetSelectable(false))
			}
			return true
		}
	}

	t.SetCell(row, 0, NewTableCell("[red] [删除] ").SetSelectable(true).
		SetClickedFunc(func() bool {
			row := t.GetRowCount()
			for i := tableStartRow; i < row; i++ {
				if t.GetCell(i, 1).Text == cols[0] {
					t.RemoveRow(i)
					return true
				}
			}
			return false
		}),
	)
	for idx, v := range cols {
		t.SetCell(row, idx+1, NewTableCell(v).SetSelectable(false))
	}

	return true
}

func (a *App) showGlobalCMOptionConfigPage(current string) {
	cmOpt := a.global.CMOption
	if cmOpt == nil {
		cmOpt = &config.CMOption{}
	}
	if a.global.BaseDir != "" {
		cmOpt.Dir = path.Join(a.global.BaseDir, "cm")
	}

	cmServerConfTable := NewTable("CMServer参数")
	for k, v := range cmOpt.CmServerConf {
		a.addTableRow(cmServerConfTable, k, v)
	}
	cmServerConfInput := NewInputField("添加CMServer参数", "", "选填, 格式: <KEY>=<VALUE>", 50, nil, nil)
	cmServerConfInput.SetDoneFunc(func(key tcell.Key) {
		if a.addKVTableRow(cmServerConfTable, cmServerConfInput.GetText()) {
			cmServerConfInput.SetText("")
			a.app.SetFocus(cmServerConfInput)
		}
	})

	cmAgentConfTable := NewTable("CMAgent参数")
	for k, v := range cmOpt.CmAgentConf {
		a.addTableRow(cmAgentConfTable, k, v)
	}
	cmAgentConfInput := NewInputField("添加CMAgent参数", "", "选填, 格式: <KEY>=<VALUE>", 50, nil, nil)
	cmAgentConfInput.SetDoneFunc(func(key tcell.Key) {
		if a.addKVTableRow(cmAgentConfTable, cmAgentConfInput.GetText()) {
			cmAgentConfInput.SetText("")
			a.app.SetFocus(cmAgentConfInput)
		}
	})

	form := NewXForm()
	form.AddInputField("CM安装目录", cmOpt.Dir, "默认值 ${安装目录}/cm", 50, nil, func(text string) { cmOpt.Dir = text }).
		AddInputField("CMServer端口", strconv.Itoa(cmOpt.CmServerPort), "默认15530，取值范围 [1024, 65535]", 50, a.validatePort, func(text string) { cmOpt.CmServerPort, _ = strconv.Atoi(text) }).
		AddInputField("虚拟IP", cmOpt.DBServiceVIP, "选填", 50, nil, func(text string) { cmOpt.DBServiceVIP = text }).
		AddFormItem(cmServerConfInput).
		AddFormItem(cmAgentConfInput)

	submitButton := center(tview.NewButton("保存").SetSelectedFunc(func() {
		if cmOpt.DBServiceVIP != "" && !a.validateIP(cmOpt.DBServiceVIP, false) {
			return
		}

		// set values
		if cmOpt.CmServerConf == nil {
			cmOpt.CmServerConf = make(map[string]string)
		}
		for i := tableStartRow; i < cmServerConfTable.GetRowCount(); i++ {
			cmOpt.CmServerConf[cmServerConfTable.GetCell(i, 1).Text] = cmServerConfTable.GetCell(i, 2).Text
		}

		if cmOpt.CmAgentConf == nil {
			cmOpt.CmAgentConf = make(map[string]string)
		}
		for i := tableStartRow; i < cmAgentConfTable.GetRowCount(); i++ {
			cmOpt.CmAgentConf[cmAgentConfTable.GetCell(i, 1).Text] = cmAgentConfTable.GetCell(i, 2).Text
		}
		a.global.CMOption = cmOpt

		// back to previus page
		a.pages.SwitchToPage(current)
		a.pages.RemovePage(pageGlobalCMOption)
	}), 6 /* space+2汉字*2+space */, 1)
	items := tview.NewFlex().SetDirection(tview.FlexRow).
		AddItem(form, 11 /* 5*(1FormItem+1空行)+1padding */, 0, false).
		AddItem(tview.NewFlex().
			AddItem(cmServerConfTable, 0, 1, false).
			AddItem(cmAgentConfTable, 0, 1, false), 0, 1, false).
		AddItem(submitButton, 1, 0, false)
	page := a.makePageWithCloseButton(items, "配置CM相关参数", current, pageGlobalCMOption, nil)
	a.pages.AddPage(pageGlobalCMOption, page, true, false)
	a.pages.SwitchToPage(pageGlobalCMOption)
}

func (a *App) fillingSubDirs() {
	if a.global.BaseDir == "" {
		return
	}

	if a.global.AppDir == "" {
		a.global.AppDir = path.Join(a.global.BaseDir, "app")
	}
	if a.global.DataDir == "" {
		a.global.DataDir = path.Join(a.global.BaseDir, "data")
	}
	if a.global.ToolDir == "" {
		a.global.ToolDir = path.Join(a.global.BaseDir, "tool")
	}
	if a.global.LogDir == "" {
		a.global.LogDir = path.Join(a.global.BaseDir, "log")
	}
	if a.global.TmpDir == "" {
		a.global.TmpDir = path.Join(a.global.BaseDir, "tmp")
	}
}

func (a *App) addExtraHBA(table *tview.Table, current string) {
	var v = &config.HBAConf{
		Type:     "host",
		Database: "all",
		User:     "all",
		Method:   "md5",
	}
	form := NewXForm().
		AddDropDown("连接类型", []string{"local", "host", "hostssl", "hostnossl"}, 1, func(option string, optionIndex int) {
			v.Type = option
		}).
		AddInputField("数据库", v.Database, "", 50, nil, func(text string) { v.Database = text }).
		AddInputField("数据库用户", v.User, "", 50, nil, func(text string) { v.User = text }).
		AddInputField("地址", v.Address, "示例: 192.168.100/32", 50, nil, func(text string) { v.Address = text }).
		AddDropDown("认证方式", []string{"trust", "reject", "md5", "sha256", "sm3", "cert", "gss", "peer"}, 2, func(option string, optionIndex int) {
			v.Method = option
		}).
		AddInputField("认证参数", "", "选填", 50, nil, func(text string) {
			v.Option = text
		}).
		AddButton("添加", func() {
			if v.Address == "" {
				a.Error("地址不能为空")
				return
			}
			a.addTableRow(table, v.Type, v.Database, v.User, v.Address, v.Method, v.Option)
			a.pages.SwitchToPage(current)
			a.pages.RemovePage(pageGlobalAddHBA)
		})
	page := a.makePageWithCloseButton(form, "添加HBA配置", current, pageGlobalAddHBA, nil)
	a.pages.AddPage(pageGlobalAddHBA, page, true, false)
	a.pages.SwitchToPage(pageGlobalAddHBA)
}

func (a *App) showGlobalAdvancedConfigPage(current string) {
	a.fillingSubDirs()
	initdbParamTable := NewTable("gs_initdb参数")
	for _, v := range a.global.GsInitdbOpts {
		a.addTableRow(initdbParamTable, v)
	}
	initdbInput := NewInputField("添加gs_initdb参数", "", "示例：--locale=<LOCALE>", 50, nil, nil)
	initdbInput.SetDoneFunc(func(key tcell.Key) {
		v := initdbInput.GetText()
		if v == "" {
			return
		}
		if a.addTableRow(initdbParamTable, v) {
			initdbInput.SetText("")
		}
	})

	envTable := NewTable("环境变量")
	for k, v := range a.global.EnvMap {
		a.addTableRow(envTable, k, v)
	}
	envInput := NewInputField("添加环境变量", "", "格式: <KEY>=<VALUE>", 50, nil, nil)
	envInput.SetDoneFunc(func(key tcell.Key) {
		v := envInput.GetText()
		if v == "" {
			return
		}
		if a.addKVTableRow(envTable, v) {
			envInput.SetText("")
		}
	})

	hbaTable := NewTable("HBA配置")
	for _, v := range a.global.ExtraHBAConf {
		a.addTableRow(hbaTable, v.Type, v.Database, v.User, v.Address, v.Method, v.Option)
	}
	// hbaEditText := tview.NewTextView().
	// 	SetLabel("额外的HBA配置").
	// 	SetRegions(true).
	// 	SetSize(1, 6).
	// 	SetDynamicColors(true).
	// 	SetWrap(false).
	// 	SetHighlightedFunc(func(added, removed, remaining []string) {
	// 		a.addExtraHBA(hbaTable, pageGlobalAdvanced)
	// 	})
	// fmt.Fprintf(hbaEditText, `["1"][blue]添加[white][""]`)
	form := NewXForm()
	form.AddInputField("应用安装目录", a.global.AppDir, "默认值 ${安装目录}/app", 50, nil, func(text string) { a.global.AppDir = text }).
		AddInputField("数据存储目录", a.global.DataDir, "默认值 ${安装目录}/data", 50, nil, func(text string) { a.global.DataDir = text }).
		AddInputField("工具存放目录", a.global.ToolDir, "默认值 ${安装目录}/tool", 50, nil, func(text string) { a.global.ToolDir = text }).
		AddInputField("日志存放目录", a.global.LogDir, "默认值 ${安装目录}/log", 50, nil, func(text string) { a.global.LogDir = text }).
		AddInputField("临时文件目录", a.global.TmpDir, "默认值 ${安装目录}/tmp", 50, nil, func(text string) { a.global.TmpDir = text }).
		AddFormItem(initdbInput).
		AddFormItem(envInput).
		AddButton("添加HBA配置", func() {
			a.addExtraHBA(hbaTable, pageGlobalAdvanced)
		})

	submitButton := center(tview.NewButton("保存").SetSelectedFunc(func() {
		// set values
		for i := tableStartRow; i < initdbParamTable.GetRowCount(); i++ {
			a.global.GsInitdbOpts = append(a.global.GsInitdbOpts, initdbParamTable.GetCell(i, 1).Text)
		}

		if a.global.EnvMap == nil {
			a.global.EnvMap = make(map[string]string)
		}
		for i := tableStartRow; i < envTable.GetRowCount(); i++ {
			a.global.EnvMap[envTable.GetCell(i, 1).Text] = envTable.GetCell(i, 2).Text
		}

		for i := tableStartRow; i < hbaTable.GetRowCount(); i++ {
			a.global.ExtraHBAConf = append(a.global.ExtraHBAConf,
				&config.HBAConf{
					Type:     hbaTable.GetCell(i, 1).Text,
					Database: hbaTable.GetCell(i, 2).Text,
					User:     hbaTable.GetCell(i, 3).Text,
					Address:  hbaTable.GetCell(i, 4).Text,
					Method:   hbaTable.GetCell(i, 5).Text,
					Option:   hbaTable.GetCell(i, 6).Text,
				})
		}

		// back to previus page
		a.pages.SwitchToPage(current)
		a.pages.RemovePage(pageGlobalAdvanced)
	}), 6, 1)

	items := tview.NewFlex().SetDirection(tview.FlexRow).
		AddItem(form, 17, 0, false).
		AddItem(tview.NewFlex().
			AddItem(initdbParamTable, 0, 1, false).
			AddItem(envTable, 0, 1, false), 0, 1, false).
		AddItem(hbaTable, 0, 1, false).
		AddItem(submitButton, 1, 0, false)
	page := a.makePageWithCloseButton(items, "高级配置", current, pageGlobalAdvanced, nil)
	a.pages.AddPage(pageGlobalAdvanced, page, true, false)
	a.pages.SwitchToPage(pageGlobalAdvanced)
}

func (a *App) showDBServerAdvancedConfigPage(current string, server *config.DBServer) {
	form := NewXForm()
	dbConfTable := NewTable("数据库参数")
	for k, v := range server.DBConf {
		a.addTableRow(dbConfTable, k, v)
	}
	dbConfInput := NewInputField("添加数据库参数", "", "格式: <KEY>=<VALUE>", 50, nil, nil)
	dbConfInput.SetDoneFunc(func(key tcell.Key) {
		v := dbConfInput.GetText()
		if v == "" {
			return
		}
		if a.addKVTableRow(dbConfTable, v) {
			dbConfInput.SetText("")
		}
	})

	envTable := NewTable("环境变量")
	for k, v := range server.EnvMap {
		a.addTableRow(envTable, k, v)
	}
	envInput := NewInputField("添加环境变量", "", "格式: <KEY>=<VALUE>", 50, nil, nil)
	envInput.SetDoneFunc(func(key tcell.Key) {
		v := envInput.GetText()
		if v == "" {
			return
		}
		if a.addKVTableRow(envTable, v) {
			envInput.SetText("")
		}
	})

	azPri := ""
	if server.AzPriority >= 1 {
		azPri = strconv.Itoa(server.AzPriority)
	}
	form.AddInputField("可用区", server.AzName, "选填，自定义值，默认值 AZ1", 50, nil, func(text string) { server.AzName = text }).
		AddInputField("可用区优先级", azPri, "选填，默认值 1, 取值越小优先级越高，最小值为 1", 50, nil, func(text string) {
			a.ClearLog()
			v, err := strconv.Atoi(text)
			if err != nil || v < 1 {
				a.Error(fmt.Sprintf("优先级的值错误: %s", text))
				return
			}
			server.AzPriority = v
		}).
		AddFormItem(dbConfInput).
		AddFormItem(envInput)

	submitButton := center(tview.NewButton("保存").SetSelectedFunc(func() {
		a.ClearLog()

		if server.DBConf == nil {
			server.DBConf = make(map[string]string)
		}
		for i := tableStartRow; i < dbConfTable.GetRowCount(); i++ {
			server.DBConf[dbConfTable.GetCell(i, 1).Text] = dbConfTable.GetCell(i, 2).Text
		}
		if server.EnvMap == nil {
			server.EnvMap = make(map[string]string)
		}
		for i := tableStartRow; i < envTable.GetRowCount(); i++ {
			server.EnvMap[envTable.GetCell(i, 1).Text] = envTable.GetCell(i, 2).Text
		}
		a.pages.SwitchToPage(current)
		a.pages.RemovePage(pageDBServerAdvanced)
	}), 6, 1)

	items := tview.NewFlex().SetDirection(tview.FlexRow).
		AddItem(form, 9, 0, false).
		AddItem(tview.NewFlex().
			AddItem(dbConfTable, 0, 1, false).
			AddItem(envTable, 0, 1, false), 0, 1, false).
		AddItem(submitButton, 1, 0, false)
	page := a.makePageWithCloseButton(items, "高级配置", current, pageDBServerAdvanced, nil)
	a.pages.AddPage(pageDBServerAdvanced, page, true, false)
	a.pages.SwitchToPage(pageDBServerAdvanced)
}

func (a *App) initLogPanel() {
	logPanel := tview.NewTextView()
	a.logPanel = logPanel
}

func (a *App) initTreePanel() {
	configNode := tview.NewTreeNode("所有配置").SetSelectable(false)
	globalNode := tview.NewTreeNode("global").SetSelectedFunc(
		func() { a.pages.SwitchToPage("page-global") },
	)

	configNode.AddChild(globalNode)
	treeView := tview.NewTreeView().
		SetRoot(configNode).
		SetGraphicsColor(tcell.ColorGreen)
	treeView.SetBorder(true)
	treeView.SetBorderPadding(1, 1, 1, 1)
	treeView.SetTitle("配置项")
	treeView.SetTitleAlign(tview.AlignLeft)
	a.treePanel = treeView
}

func (a *App) validatePort(s string, _ rune) bool {
	a.ClearLog()
	v, err := strconv.Atoi(s)
	if err == nil && (v >= 1 && v <= 65535) {
		return true
	}
	a.Error(fmt.Sprintf("端口 %s 不合法", s))
	return false
}

func (a *App) serverPageName(instType, host string) string {
	return fmt.Sprintf(pageServer, instType, host)
}

func (a *App) dbServerPageName(host string) string {
	return a.serverPageName("db", host)
}

func (a *App) cmServerPageName(host string) string {
	return a.serverPageName("cm", host)
}

func (a *App) deleteDBServerPage(host string) {
	a.pages.RemovePage(a.dbServerPageName(host))
	for _, node := range a.dbServersNode.GetChildren() {
		if strings.Contains(node.GetText(), host) {
			a.dbServersNode.RemoveChild(node)
			break
		}
	}
	for idx, s := range a.dbServers {
		if s.Host == host {
			a.dbServers = append(a.dbServers[:idx], a.dbServers[idx+1:]...)
			break
		}
	}
	if len(a.dbServersNode.GetChildren()) == 0 {
		a.treePanel.GetRoot().RemoveChild(a.dbServersNode)
	}
}

func (a *App) deleteCMServerPage(host string) {
	a.pages.RemovePage(a.cmServerPageName(host))
	for _, node := range a.cmServersNode.GetChildren() {
		if node.GetText() == host {
			a.cmServersNode.RemoveChild(node)
			break
		}
	}
	for idx, s := range a.cmServers {
		if s.Host == host {
			a.cmServers = append(a.cmServers[:idx], a.cmServers[idx+1:]...)
			break
		}
	}
	if len(a.cmServersNode.GetChildren()) == 0 {
		a.treePanel.GetRoot().RemoveChild(a.cmServersNode)
	}
}

func (a *App) appendDBServerPage(server *config.DBServer) {
	initialRoleIdx := 0
	for idx, role := range dbRoles {
		if server.Role == role {
			initialRoleIdx = idx
		}
	}
	upstreamDropDown := tview.NewDropDown()
	upstreamDropDown.SetFieldBackgroundColor(tcell.ColorBlack).
		SetLabel("上游库IP")
	form := NewXForm().
		AddInputField("服务器IP", server.Host, "", 50, nil, func(text string) { server.Host = text }).
		AddDropDown("数据库角色", dbRoles, initialRoleIdx, func(option string, optionIndex int) {
			server.Role = option
			if option == dbRoleCascadeStandby {
				var standbys []string
				for _, s := range a.dbServers {
					if s.Role == dbRoleStandby {
						standbys = append(standbys, s.Host)
					}
				}
				if len(standbys) > 0 {
					upstreamDropDown.
						SetOptions(standbys, func(text string, index int) {
							server.UpstreamHost = text
						}).
						SetCurrentOption(0)
				}
			}
		}).
		AddInputField("用户名", server.User, "", 50, nil, func(text string) { server.User = text }).
		AddInputField("用户组", server.Group, "", 50, nil, func(text string) { server.Group = text }).
		AddInputField("用户密码", server.UserPassword, "", 50, nil, func(text string) { server.UserPassword = text }).
		AddInputField("数据库端口", strconv.Itoa(server.DBPort), "", 50, a.validatePort,
			func(s string) { server.DBPort, _ = strconv.Atoi(s) }).
		AddInputField("备用IP", strings.Join(server.HAIPs, ","), "", 50, nil, func(text string) { server.HAIPs = strings.Split(text, ",") }).
		AddFormItem(upstreamDropDown)
	a.addSSHFormItems(form, server.SSHOption)
	form.AddButton("高级配置", func() { a.showDBServerAdvancedConfigPage(a.dbServerPageName(server.Host), server) })

	deleteBtn := tview.NewButton("删除实例").SetSelectedFunc(func() {
		currentFocus := a.app.GetFocus()
		modal := tview.NewModal().
			SetText(fmt.Sprintf("确定要删除DB实例 %s 吗?", server.Host)).
			AddButtons([]string{"确定", "取消"}).
			SetDoneFunc(func(buttonIndex int, buttonLabel string) {
				if buttonLabel == "确定" {
					a.deleteDBServerPage(server.Host)
					a.pages.SwitchToPage(pageGlobal)
				} else {
					a.app.SetFocus(currentFocus)
				}
				a.pages.RemovePage(pageModal)
			})
		a.pages.AddPage(pageModal, modal, true, true)
		a.app.SetFocus(modal)
	}).SetStyle(tcell.StyleDefault.Background(tcell.ColorDarkRed).Foreground(tcell.ColorWhite))
	serverPage := tview.NewFlex().SetDirection(tview.FlexRow).
		AddItem(form, 0, 1, false).
		AddItem(center(deleteBtn, 10, 1), 1, 0, false)
	serverPage.SetBorder(true).SetBorderPadding(1, 1, 2, 2).
		SetTitle(fmt.Sprintf("DB实例 %s", server.Host)).SetTitleAlign(tview.AlignCenter)
	a.pages.AddPage(a.dbServerPageName(server.Host), serverPage, true, false)

	serverNode := tview.NewTreeNode(fmt.Sprintf("%s(%s)", server.Host, shortRoleMap[server.Role])).
		SetSelectedFunc(func() {
			a.pages.SwitchToPage(a.dbServerPageName(server.Host))
		})
	if a.dbServersNode == nil {
		a.dbServersNode = tview.NewTreeNode("db_servers").SetSelectedFunc(func() {
			if a.dbServersNode.IsExpanded() {
				a.dbServersNode.Collapse()
			} else {
				a.dbServersNode.Expand()
			}
		})
		a.treePanel.GetRoot().AddChild(a.dbServersNode)
	}
	a.dbServersNode.AddChild(serverNode)
}

func (a *App) appendCMServerPage(server *config.CMServer) {
	form := NewXForm().
		AddInputField("服务器IP", server.Host, "", 50, nil, func(text string) { server.Host = text }).
		AddInputField("CMServer端口", strconv.Itoa(server.Port), "", 50, a.validatePort, func(text string) { server.Port, _ = strconv.Atoi(text) })
	a.addSSHFormItems(form, server.SSHOption)

	deleteBtn := tview.NewButton("删除实例").SetSelectedFunc(func() {
		currentFocus := a.app.GetFocus()
		modal := tview.NewModal().
			SetText(fmt.Sprintf("确定要删除CM实例 %s 吗?", server.Host)).
			AddButtons([]string{"确定", "取消"}).
			SetDoneFunc(func(buttonIndex int, buttonLabel string) {
				if buttonLabel == "确定" {
					a.deleteCMServerPage(server.Host)
					a.pages.SwitchToPage(pageGlobal)
				} else {
					a.app.SetFocus(currentFocus)
				}
				a.pages.RemovePage(pageModal)
			})
		a.pages.AddPage(pageModal, modal, true, true)
		a.app.SetFocus(modal)
	}).SetStyle(tcell.StyleDefault.Background(tcell.ColorDarkRed).Foreground(tcell.ColorWhite))
	serverPage := tview.NewFlex().SetDirection(tview.FlexRow).
		AddItem(form, 0, 1, false).
		AddItem(center(deleteBtn, 10, 1), 1, 0, false)
	serverPage.SetBorder(true).SetBorderPadding(1, 1, 2, 2).
		SetTitle(fmt.Sprintf("CM实例 %s", server.Host)).SetTitleAlign(tview.AlignCenter)
	a.pages.AddPage(a.cmServerPageName(server.Host), serverPage, true, false)

	serverNode := tview.NewTreeNode(server.Host).
		SetSelectedFunc(func() {
			a.pages.SwitchToPage(a.cmServerPageName(server.Host))
		})
	if a.cmServersNode == nil {
		a.cmServersNode = tview.NewTreeNode("cm_servers").SetSelectedFunc(func() {
			if a.cmServersNode.IsExpanded() {
				a.cmServersNode.Collapse()
			} else {
				a.cmServersNode.Expand()
			}
		})
		a.treePanel.GetRoot().AddChild(a.cmServersNode)
	}
	a.cmServersNode.AddChild(serverNode)
}

func (a *App) addSSHFormItems(form *XForm, sshOpt *config.SSHOption) {
	form.AddInputField("SSH端口", strconv.Itoa(sshOpt.Port), "默认值 22", 50, a.validatePort, func(text string) { sshOpt.Port, _ = strconv.Atoi(text) }).
		AddInputField("SSH用户", sshOpt.User, "默认值 root", 50, nil, func(text string) { sshOpt.User = text }).
		AddInputField("SSH密码", sshOpt.Password, "密码和密钥二选一即可", 50, nil, func(text string) { sshOpt.Password = text }).
		AddInputField("SSH私钥路径", sshOpt.KeyFile, "密码和密钥二选一即可，默认 ${HOME}/.ssh/id_rsa", 50, nil, func(text string) { sshOpt.KeyFile = text }).
		AddInputField("SSH私钥密码", sshOpt.Passphrase, "", 50, nil, func(text string) { sshOpt.Passphrase = text })
}

func (a *App) showAddCMServerPage(current string) {
	cmServer := &config.CMServer{
		Port: 15300,
	}
	if a.global.CMOption != nil && a.global.CMOption.CmServerPort != 0 {
		cmServer.Port = a.global.CMOption.CmServerPort
	}
	if cmServer.SSHOption == nil && a.global.SSHOption != nil {
		cmServer.SSHOption = a.global.SSHOption
	}

	form := NewXForm().
		AddInputField("服务器IP", "", "支持IPv4，127.0.0.1和localhost", 50, nil, func(text string) { cmServer.Host = text }).
		AddInputField("CMServer端口", strconv.Itoa(cmServer.Port), "", 10, a.validatePort, func(text string) { cmServer.Port, _ = strconv.Atoi(text) })
	a.addSSHFormItems(form, cmServer.SSHOption)
	form.AddButton("添加", func() {
		a.ClearLog()
		for _, s := range a.cmServers {
			if s.Host == cmServer.Host {
				a.Error(fmt.Sprintf("CM实例 %s 已存在", s.Host))
				return
			}
		}
		if !a.validateIP(cmServer.Host, true) {
			return
		}
		a.cmServers = append(a.cmServers, cmServer)
		a.appendCMServerPage(cmServer)
		a.pages.SwitchToPage(a.cmServerPageName(cmServer.Host))
		a.pages.RemovePage(pageAddCMServer)
	})
	addServerPage := a.makePageWithCloseButton(form, "添加CM实例", current, pageAddCMServer, nil)
	a.pages.AddPage(pageAddCMServer, addServerPage, true, true)
	a.pages.SwitchToPage(pageAddCMServer)
}

func (a *App) makePageWithCloseButton(p tview.Primitive, title, prevPage, closePage string, closeCallback func()) *tview.Flex {
	closeButton := tview.NewButton("关闭").SetSelectedFunc(func() {
		a.ClearLog()
		if closeCallback != nil {
			closeCallback()
		}
		a.pages.SwitchToPage(prevPage)
		a.pages.RemovePage(closePage)
	}).SetStyle(tcell.StyleDefault.Background(tcell.ColorDarkRed).Foreground(tcell.ColorWhite))
	panel := tview.NewFlex().
		SetDirection(tview.FlexRow).
		AddItem(tview.NewFlex().
			AddItem(nil, 0, 8, false).
			AddItem(closeButton, 8, 0, false), 1, 0, false).
		AddItem(p, 0, 1, false)
	panel.SetBorder(true).
		SetTitle(title).
		SetTitleAlign(tview.AlignCenter).
		SetBorderPadding(1, 1, 2, 2)
	return panel
}

func (a *App) showAddDBServerPage(current string) {
	sshOpt := a.global.SSHOption
	if sshOpt == nil {
		sshOpt = &config.SSHOption{
			Port: 22,
			User: "root",
		}
	}
	dbServer := &config.DBServer{
		User:         a.global.User,
		Group:        a.global.Group,
		UserPassword: a.global.UserPassword,
		DBPort:       a.global.DBPort,
		SSHOption:    sshOpt,
	}
	var cmServer *config.CMServer
	var initialRoleIdx int
	for _, s := range a.dbServers {
		if s.Role == dbRolePrimary {
			initialRoleIdx = 1
			break
		}
	}

	upstreamDropDown := tview.NewDropDown()
	upstreamDropDown.SetFieldBackgroundColor(tcell.ColorBlack).
		SetLabel("上游库IP")

	form := NewXForm()
	form.AddInputField("服务器IP", "", "支持IPv4，127.0.0.1和localhost", 50, nil, func(text string) { dbServer.Host = text }).
		AddDropDown("数据库角色", dbRoles, initialRoleIdx, func(option string, optionIndex int) {
			a.ClearLog()
			dbServer.Role = option
			if option == dbRoleCascadeStandby {
				var standbys []string
				for _, s := range a.dbServers {
					if s.Role == dbRoleStandby {
						standbys = append(standbys, s.Host)
					}
				}
				if len(standbys) == 0 {
					a.Error("没有备库可供选择，请先添加备库，然后再添加级联备")
					return
				}
				if len(standbys) > 0 {
					upstreamDropDown.
						SetOptions(standbys, func(text string, index int) {
							dbServer.UpstreamHost = text
						}).
						SetCurrentOption(0)
				}
			}
		}).
		AddInputField("用户名", dbServer.User, "选填，默认从 global 继承", 50, nil, func(text string) { dbServer.User = text }).
		AddInputField("用户组", dbServer.Group, "选填，默认从 global 继承", 50, nil, func(text string) { dbServer.Group = text }).
		AddInputField("数据库端口", strconv.Itoa(dbServer.DBPort), "", 10, a.validatePort,
			func(s string) { dbServer.DBPort, _ = strconv.Atoi(s) }).
		AddInputField("备用IP", "", "选填", 50, nil, func(text string) { dbServer.HAIPs = strings.Split(text, ",") }).
		AddFormItem(upstreamDropDown)
	a.addSSHFormItems(form, sshOpt)
	form.AddCheckbox("同时添加CM实例", false, func(checked bool) {
		idx := form.GetFormItemIndex("CMServer端口")
		if !checked {
			cmServer = nil
			if idx != -1 {
				form.RemoveFormItem(idx)
			}
			return

		}
		cmServer = &config.CMServer{
			Host:      dbServer.Host,
			Port:      15300,
			SSHOption: sshOpt,
		}
		if a.global.CMOption != nil && a.global.CMOption.CmServerPort != 0 {
			cmServer.Port = a.global.CMOption.CmServerPort
		}
		form.AddInputField("CMServer端口", strconv.Itoa(cmServer.Port), "", 10, a.validatePort, func(text string) {
			cmServer.Port, _ = strconv.Atoi(text)
		})
	}).
		AddButton("添加", func() {
			a.ClearLog()
			for _, s := range a.dbServers {
				if s.Host == dbServer.Host {
					a.Error(fmt.Sprintf("实例 %s 已存在", dbServer.Host))
					return
				}
			}
			if !a.validateIP(dbServer.Host, true) {
				return
			}

			if dbServer.Role == dbRoleCascadeStandby && dbServer.UpstreamHost == "" {
				a.Error("级联备库需指定上游备库IP")
				return
			}

			if sshOpt.Password == "" || sshOpt.KeyFile == "" {
				sshOpt = nil
			}
			a.dbServers = append(a.dbServers, dbServer)
			a.appendDBServerPage(dbServer)
			if cmServer != nil {
				a.cmServers = append(a.cmServers, cmServer)
				a.appendCMServerPage(cmServer)
			}
			a.pages.SwitchToPage(a.dbServerPageName(dbServer.Host))
			a.pages.RemovePage(pageAddDBServer)
		})

	page := a.makePageWithCloseButton(form, "添加DB实例", current, pageAddDBServer, nil)
	a.pages.AddPage(pageAddDBServer, page, true, true)
	a.pages.SwitchToPage(pageAddDBServer)
}

func (a *App) validateIP(host string, allowLocal bool) bool {
	a.ClearLog()
	if host == "" {
		a.Error("服务器IP不能为空")
		return false
	}

	if allowLocal && (host == "127.0.0.1" || host == "localhost") {
		return true
	}

	ip := net.ParseIP(host)
	if ip == nil {
		a.Error("服务器IP不合法")
		return false
	}
	if ip.To4() == nil {
		a.Error("服务器IP仅支持IPv4")
		return false
	}

	return true
}

func (a *App) showPreviewPage(current string) {
	previewText := tview.NewTextView()
	previewText.ScrollToBeginning()
	previewText.SetText(a.toYAML())
	page := a.makePageWithCloseButton(previewText, "预览YAML", current, pagePreview, nil)
	a.pages.AddPage(pagePreview, page, true, true)
	a.pages.SwitchToPage(pagePreview)
}

func (a *App) initBanner() {
	title := center(tview.NewTextView().
		SetDynamicColors(true).
		SetText(`[green] PTK图形化配置工具`), 19, 1)
	output := tview.NewTextView().
		SetText(fmt.Sprintf("目标文件：%s", a.outFile)).
		SetTextAlign(tview.AlignRight)
	a.banner = tview.NewFlex().
		AddItem(title, 19, 0, false).
		AddItem(output, 0, 1, false)
}

func (a *App) initAppPanel() {
	root := tview.NewFlex().SetDirection(tview.FlexRow).
		AddItem(a.banner, 1, 0, false).
		AddItem(
			tview.NewFlex().
				AddItem(a.treePanel, 29, 0, false).
				AddItem(a.pages, 0, 1, false),
			0, 1, false).
		AddItem(a.oprPanel, 3, 0, false).
		AddItem(a.logPanel, 1, 0, false)
	a.appPanel = center(root, 120, 40)
}

func (a *App) initOprPanel() {
	addDBServerBtn := tview.NewButton("添加DB实例").SetSelectedFunc(func() {
		if a.pages.HasPage(pageAddDBServer) {
			a.pages.SwitchToPage(pageAddDBServer)
			return
		}
		currentPage, _ := a.pages.GetFrontPage()
		a.showAddDBServerPage(currentPage)
	})

	addCMServerBtn := tview.NewButton("添加CM实例").SetSelectedFunc(func() {
		if a.pages.HasPage(pageAddCMServer) {
			a.pages.SwitchToPage(pageAddCMServer)
			return
		}
		currentPage, _ := a.pages.GetFrontPage()
		a.showAddCMServerPage(currentPage)
	})

	previewBtn := tview.NewButton("预览YAML").SetSelectedFunc(func() {
		currentPage, _ := a.pages.GetFrontPage()
		if currentPage == pagePreview {
			return
		}
		a.showPreviewPage(currentPage)
	})

	oprBtnGroup := tview.NewFlex().
		AddItem(center(addDBServerBtn, 10, 1), 0, 1, false).
		AddItem(center(addCMServerBtn, 10, 1), 0, 1, false).
		AddItem(center(previewBtn, 10, 1), 0, 1, false)
	oprBtnGroup.SetTitleAlign(tview.AlignLeft).
		SetTitle("操作").
		SetBorder(true)

	exitBtn := tview.NewButton("退出").SetSelectedFunc(func() {
		a.app.Stop()
	}).SetStyle(tcell.StyleDefault.Background(tcell.ColorDarkRed).Foreground(tcell.ColorWhite))
	saveBtn := tview.NewButton("完成").SetSelectedFunc(func() {
		a.dump()
		a.app.Stop()
	}).SetStyle(tcell.StyleDefault.Background(tcell.ColorGreen).Foreground(tcell.ColorWhite))
	exitBtnGroup := tview.NewFlex().
		AddItem(center(exitBtn, 6, 1), 0, 1, false).
		AddItem(center(saveBtn, 6, 1), 0, 1, false)
	exitBtnGroup.SetBorder(true)
	a.oprPanel = tview.NewFlex().
		AddItem(oprBtnGroup, 0, 4, false).
		AddItem(exitBtnGroup, 0, 2, false)
	a.oprPanel.SetBorderPadding(0, 0, 1, 1)
}

func (a *App) checkOutFile() error {
	f, err := os.OpenFile(a.outFile, os.O_CREATE|os.O_RDWR|os.O_TRUNC, 0666)
	if err != nil {
		return err
	}
	defer f.Close()
	return nil
}

func (a *App) dump() {
	f, err := os.OpenFile(a.outFile, os.O_CREATE|os.O_RDWR|os.O_TRUNC, 0666)
	if err != nil {
		a.Error(fmt.Sprintf("打开文件 %s 失败: %s", a.outFile, err))
		return
	}
	defer f.Close()

	_, err = f.WriteString(a.toYAML())
	if err != nil {
		a.Error(fmt.Sprintf("写入文件 %s 失败: %s", a.outFile, err))
		return
	}
}

func (a *App) ClearLog() {
	a.logPanel.Clear()
}

func (a *App) Warn(msg string) {
	a.log("WARN", msg, "yellow")
}

func (a *App) Error(msg string) {
	a.log("ERROR", msg, "red")
}

func (a *App) log(level, msg string, color string) {
	currentTime := time.Now()
	formattedTime := currentTime.Format("2006-01-02 15:04:05")
	a.logPanel.SetText("[" + formattedTime + "] [" + level + "] " + msg).SetTextColor(tcell.GetColor(color))
}

//================= themes area ================

var fieldBackgroundColor = tcell.ColorBlack

var placeholderStyle = tcell.Style{}.
	Foreground(tcell.ColorDefault).
	Background(fieldBackgroundColor)

var placeholderTextColor = tcell.ColorGray

var baseTheme tview.Theme = tview.Theme{
	PrimitiveBackgroundColor:    tcell.ColorBlack,
	ContrastBackgroundColor:     tcell.ColorBlue,
	MoreContrastBackgroundColor: tcell.ColorGreen,
	BorderColor:                 tcell.ColorWhite,
	TitleColor:                  tcell.ColorWhite,
	GraphicsColor:               tcell.ColorWhite,
	PrimaryTextColor:            tcell.ColorWhite,
	SecondaryTextColor:          tcell.ColorYellow,
	TertiaryTextColor:           tcell.ColorGreen,
	InverseTextColor:            tcell.ColorBlue,
	ContrastSecondaryTextColor:  tcell.ColorNavy,
}

func center(p tview.Primitive, w, h int) tview.Primitive {
	return tview.NewFlex().
		AddItem(nil, 0, 1, false). // left
		AddItem(
			tview.NewFlex().SetDirection(tview.FlexRow).
				AddItem(nil, 0, 1, false). // top
				AddItem(p, h, 0, false).
				AddItem(nil, 0, 1, false), // bottom
						w, 0, false).
		AddItem(nil, 0, 1, false) // right
}

func NewTableCell(text string) *tview.TableCell {
	c := tview.NewTableCell(text).
		SetAlign(tview.AlignCenter).
		SetSelectable(false)
	return c
}

func NewTable(title string) *tview.Table {
	t := tview.NewTable()
	t.SetEvaluateAllRows(true).
		SetSelectable(false, false).
		ScrollToBeginning()

	// Box methods
	t.SetTitle(title).
		SetTitleAlign(tview.AlignLeft).
		SetTitleColor(tcell.ColorYellow).
		SetBorder(true)
	return t
}

func NewInputField(label, value, placeholder string, fieldWidth int, accept func(textToCheck string, lastChar rune) bool, changed func(text string)) *tview.InputField {
	inputField := tview.NewInputField()
	inputField.SetFieldStyle(tcell.StyleDefault.Background(tcell.ColorWhite).Foreground(tcell.ColorBlack)).
		SetLabel(label).
		SetText(value).
		SetFieldWidth(fieldWidth).
		SetAcceptanceFunc(accept).
		SetChangedFunc(changed)
	inputField.SetPlaceholder(placeholder).
		SetPlaceholderStyle(placeholderStyle).
		SetPlaceholderTextColor(placeholderTextColor)
	return inputField
}

// Form customizations
type XForm struct {
	*tview.Form
}

func NewXForm() *XForm {
	return &XForm{
		tview.NewForm().
			SetFieldBackgroundColor(tcell.ColorBlack).
			SetButtonsAlign(tview.AlignLeft),
	}
}

func (f *XForm) AddTextView(label, text string, fieldWidth, fieldHeight int, dynamicColors, scrollable bool) *XForm {
	if fieldHeight == 0 {
		fieldHeight = tview.DefaultFormFieldHeight
	}

	textView := tview.NewTextView()
	f.AddFormItem(textView.
		SetLabel(label).
		SetSize(fieldHeight, fieldWidth).
		SetDynamicColors(dynamicColors).
		SetScrollable(scrollable).
		SetText(text))
	return f
}

func (f *XForm) AddInputField(label, value, placeholder string, fieldWidth int, accept func(textToCheck string, lastChar rune) bool, changed func(text string)) *XForm {
	inputField := tview.NewInputField()
	f.AddFormItem(inputField.
		SetFieldStyle(tcell.StyleDefault.Background(tcell.ColorWhite).Foreground(tcell.ColorBlack)).
		SetLabel(label).
		SetText(value).
		SetFieldWidth(fieldWidth).
		SetAcceptanceFunc(accept).
		SetChangedFunc(changed))
	inputField.SetPlaceholder(placeholder).
		SetPlaceholderStyle(placeholderStyle).
		SetPlaceholderTextColor(placeholderTextColor)
	return f
}

func (f *XForm) AddPasswordField(label, value string, placeholder string, fieldWidth int, changed func(text string)) *XForm {
	input := tview.NewInputField()
	input.SetPlaceholderStyle(placeholderStyle).
		SetPlaceholderTextColor(placeholderTextColor).
		SetPlaceholder(placeholder)
	f.AddFormItem(input.
		SetFieldTextColor(tcell.ColorBlack).
		SetLabel(label).
		SetText(value).
		SetFieldWidth(fieldWidth).
		SetMaskCharacter('*').
		SetChangedFunc(changed))
	return f
}

func (f *XForm) AddDropDown(label string, options []string, initialOption int, selected func(option string, optionIndex int)) *XForm {
	dropdown := tview.NewDropDown()
	dropdown.
		SetFieldBackgroundColor(tcell.ColorBlack).
		SetLabel(label).
		SetOptions(options, selected).
		SetCurrentOption(initialOption)

	f.AddFormItem(dropdown)
	return f
}

func (f *XForm) AddCheckbox(label string, checked bool, changed func(checked bool)) *XForm {
	f.AddFormItem(
		tview.NewCheckbox().
			SetCheckedString("(✓)").
			SetUncheckedString("( )").
			SetLabel(label).
			SetChecked(checked).
			SetChangedFunc(changed),
	)

	return f
}

func (f *XForm) AddFormItem(p tview.FormItem) *XForm {
	f.Form.AddFormItem(p)
	return f
}
